dedecms v5.7 后台代码执行
前言
这是 dedecms 5.7 的一个后台任意代码执行的漏洞,因为整个处理过程比较长,所以认真地去跟了一遍 : )。
环境:
dedecms 5.7 源码
apache 2.4.38 + php 5.6.40
vscode + xdebug
审计学习
参考文章的 payload 如下:
1 | /dede/tag_test_action.php?url=a&token=&partcode={dede:ll name='source' runphp='yes'}phpinfo();{/dede:ll} |
这里的 /dede 目录是默认的后台管理目录,所以我们需要先登录后台。
接着看一下/dede/tag_test_action.php
文件。
1 | require(dirname(__FILE__)."/config.php"); |
在文件开头包含了/dede/config.php
文件,其中定义了 csrf_check() 函数。
1 | function csrf_check() |
这里可以看到该函数检测是否设置了 token,并且等于 SESSION 中的 token。
我们在/dede/tag_test_action.php
中 csrf_token() 的调用处打一个断点,用 payload 请求该页面。
可以看到 SESSION 中的 token 默认为空,所以只要我们传入的 token 为空,就不会执行 exit。
继续看/dede/tag_test_action.php
,从 payload 中可以看出我们可控的主要是 partcode 变量,这里我就仅展示相关代码。
这里 typeid 默认为0,所以直接实例化了一个 PartView 类。在其构造函数中,主要关注 $this->dtp。
1 | $this->dtp = new DedeTagParse(); |
看一下 DedeTagParse 类中定义的属性及其构造函数。
1 | var $NameSpace = 'dede'; //标记的名字空间 |
回到tag_test_action.php
中,在实例化 PartView 后,接着调用了其中的 SetTemplet 方法,并传入了 partcode。
1 | $pv->SetTemplet($partcode, "string"); |
跟一下该方法。其中又调用了 dtp (DedeTagParse类) 的 LoadSource 方法,这里的 temp 是我们前面传入的 partcode。
1 | function SetTemplet($temp, $stype="file") |
跟进 LoadSource()。
1 | function LoadSource($str) |
LoadSource 方法根据我们传入的 partcode 拼接了一个文件名,并将 partcode 写入该文件中。所以在 /data/tplcache/ 目录中多了一个 inc 文件,下面称其为 inc1 文件。其中是我们构造的 payload。
为了之后的测试方便,我将 /data/tplcache/ 目录下的所有 inc 和 txt 文件都删除了。
在 LoadSource() 的最后调用了 dtp 的 LoadTemplate() 方法,并传入了刚刚创建的文件名。继续跟进 LoadTemplate 方法。
在该方法中,将我们一开始构造的 partcode 读入了 dtp 中的 SourceString 属性中。然后进入 LoadCache 方法。如果是第一次使用 payload,即在 /data/tplcache/ 中还没有存入相应的 cache 文件,则在执行完 LoadCache() 后会进入 else 分支,去执行 ParseTemplet()。跟进 LoadCache 方法。
这里又拼接了两个文件,分别是 inc 和 txt 文件,这里生成的 inc 文件,下面称其为 inc2 文件。因为是第一次使用 payload,在 /data/tplcache/ 目录下只有前面生成的 inc1 文件,所以 LoadCache() 在第一次返回 false。并没有执行之后的代码。等分析完第一次使用 payload 后,我会对 LoadCache() 之后的代码进行分析。
接着返回 LoadTemplate() 中。在 else 分支中,我们进入了 dtp 的 ParseTemplet 方法。
这里实例化了一个 DedeAttributeParse 类,主要负责对标签中的属性进行解析。看一下这个类中的基本属性的默认值。
1 | var $sourceString = ""; |
接着是一个 for 循环,其中主要做了以下几件事:
- 由开始标签
{dede:
找到标签名ll
,存入变量 tTagName 中。 - 匹配结束标签
/}
或者{/dede:ll}
,这里因为标签间有文本内容phpinfo();
,所以 payload 以{\dede:ll}
结尾。 - 匹配标签名和属性等信息,存入变量 attStr 中;匹配标签间的文本内容,存入变量 innerText。
- 调用 cAtt 的 SetSource 方法,其中传入 attStr,此时 attStr 中的值是
ll name='source' runphp='yes'
。将 cAtt 中的属性 cAttributes 实例化成 DedeAttribute 类,该类中属性的默认值为:$Count = -1; $Items = ""
,将传入的 attStr 存入 cAtt->sourceString 中。 - 在 cAtt 的 SetSource 方法的最后,调用了 cAtt 的ParseAttribute 方法。在该方法中,将前面实例化成 DedeAttribute 类的 cAttributes 中的属性 Items 设置为一个数组,其中存放了标签名
ll
和相应的各属性对。而 Count 是对其中的属性个数进行计数(不含标签名ll
)。
- 回到 for 循环中。
1 | $cAtt->SetSource($attStr); // 设置属性 |
最后进入了 dtp 的 SaveCache 方法,该方法和前面的 LoadCache 方法对应,其中创建了前面 LoadCache() 验证时不存在的 txt 文件,并将文件修改时间,即在 LoadCache() 中保存到 dtp 的属性 TempMkTime 中的值写入该 txt 文件。
之后根据在上面 for 循环时,存入 dtp 的属性 CTags 中的 DedeTag 类将标签信息写入 inc2 文件。在写之前会对标签间的文本内容phpinfo();
进行检查。
这里因为 DEDEDISFUN 常量默认是没有定义的,所以并没有对我们的文本内容phpinfo();
进行检查。
SaveCache() 执行完后,inc2 文件中就存有标签的的相应信息了。
并且我们会回到一开始定义的 PartView 对象(pv变量)的 SetTemplet 方法中,执行 PartView 类的 ParseTemplet 方法。之后回到最开始的/dede/tag_test_action.php
中 ,先输出一些页面的显示格式,最后调用 PartView 的 Display 方法。
之后的调用过程如下:
pv (PartView类) 的 Display()
–> dtp (DedeTagParse类) 的 Display()
—>dtp 的 GetResult()
—> dtp 的 AssignSysTag()
在 AssignSysTag() 中,从 dtp 的属性 CTags (DedeTag类) 中取出 TagName ,payload 中对应的是ll
。进行相应的判断,如果属性 runphp 的值是 yes 则进入 RunPHP 方法。
这里就说明了为什么 payload 中存在 runphp。跟进 RunPHP()。
这里的 refObj 其实就是前面的 CTag (DedeTag类) ,里面存放了标签中的信息,这里取出其中的文本内容,即phpinfo();
,并传入 eval() 中执行。
经过上面的分析,可以知道 payload 中的条件主要包含以下几个:
- 标签名不是
global
、include
、foreach
、var
,才能在最后的 AssignSysTag() 中不进入 if-else if 的分支中。 - 标签含有 runphp 属性,且值为
yes
。
但其实当标签名是global
时,在 AssignSysTag() 中进入第一个 if 分支,也可以造成任意代码执行。这里直接看该分支中的内容。
payload :/dede/tag_test_action.php?token=&partcode={dede:global%20function=%271;phpinfo()%27/}
因为该 payload 不需要将任意执行的代码放进标签间的文本中,所以这里我用了前面说的第二种形式的标签写法,其类似 html 中的<img xxx />
。
到这里,整个 payload 的处理流程就结束了。
但是前面说了这是第一次使用 payload 的处理流程。在第二篇参考文章中,走的是已经使用过 payload 的处理流程。前面部分大致是:
pv (PartView 类) 的 SetTemplet()
—> dtp (DedeTagParse 类) 的 LoadSource()
—> dtp 的 LoadTemplate()
—> dtp 的 LoadCache()
。
在 LoadCache() 中,因为已经使用过 payload,在 /data/tplcache/ 目录下存在 inc2 文件和 txt 文件,所以会继续往下执行,而不会在检查 inc2 和 txt 文件时返回 false。
这里引入的$this->CacheFile
,即前面创建的 inc2 文件。
接下来的代码将其中的数据还原成相应的类和属性,存入 dtp 的属性 Ctags 中,剩下的就和前面一样了。只不过第一次使用 payload 时,会将标签的内容进行一系列的解析,而第二次直接从 inc2 文件中读取并还原标签信息。
总结
该漏洞一开始主要是因为 csrf_token() 没有起到检测身份的作用,且 partcode 可控,并在最后解析时没有过滤就传入了 eval() 中,造成了可以利用 csrf 实现任意代码执行。
关于代码审计,还是要多去思考,多去复现,认真跟踪一遍可控变量的处理过程,才能加深理解 : )。
ref :
https://mochazz.github.io/2018/03/29/dedecms%E6%9C%80%E6%96%B0%E5%90%8E%E5%8F%B0getshell/
https://xz.aliyun.com/t/2224
Author: ll
Link: http://yoursite.com/2019/04/24/dedecms v5.7 后台代码执行/
License: 知识共享署名-非商业性使用 4.0 国际许可协议